パラレルセットグラフ#

パラレルセットグラフParallel Set Graph ) とは、複数の質的変数に対して、それらの内訳を 棒の長さ で表現する可視化手法です。 三つ以上の質的変数に対しても適用可能であるという利点があります。 パラレルセットグラフを作成する上でのポイントは、最も強調したい質的変数を左側に配置し、かつそれを基準に配色すること[Wilke et al., 2022]です.

上図の年代別・メーカー別・発売曜日別のゲームパッケージ数を表現した例を用いて説明します。 パラレルセットグラフは、一つ目の 位置 スケール(上図「位置②A」)で一つ目の質的変数(上図「発売曜日」)を指定し、 それと直交する二つ目の 位置 スケール(上図「位置②B」)でその水準(上図「木」曜日)に対応する量的変数の数量を表現します。

パラレルセットグラフでは、一番左側の質的変数(上図「年代」)を基準に配色することが推奨されています[Wilke et al., 2022]。 この性質を利用すると、 スケールと 位置 スケール(上図「位置②C」)を用いて、それ以外の質的変数内の割合(上図「木」曜日中の「2010」年代の割合)を表現することができます。

同時に扱う質的変数の種類が多いため、パラレルセットグラフでは レコードの重複カウント による内訳の歪みが発生しがちです。 例えば、上図では三つの観点(発売年代、メーカー名、発売曜日)のゲームパッケージ数の内訳を表現しています。 もしデータ中に複数の年代あるいはメーカー名あるいは発売曜日に属するゲームパッケージが存在すると、それぞれ別のレコードとして集計されてしまうため、実際の内訳からズレが生じてしまいます。 内訳を扱う手法では常に重複カウントについて注意が必要ですが、パラレルセットグラフでは扱う質的変数の種類が多いため、特に注意しましょう。

また、パラレルセットグラフで表現できる質的変数の数に上限はありませんが、あまり多すぎると見づらくなってしまいます。 目的や用途に応じて、可視化対象を厳選する必要があることはパラレルセットグラフでも変わりません。

Plotlyでは、plotly.express.parallel_categoriesでパラレルセットグラフを作成できます。

# plotly.expressモジュールをpxという名前でインポート
# 簡単にインタラクティブな図を作成するためのモジュール
import plotly.express as px

# px.parallel_categories関数を使用して、パラレルセットグラフを作成
# データフレームdf中の、dimensionsで指定した列の組み合わせに
# 該当するレコード数を棒の長さで表現
# colorで任意の列に応じた配色を指定可能だが、通常dimensionsの第一成分が推奨される
fig = px.parallel_categories(df, dimensions=["col_0", "col_1", "col_2"], color="col_0")

初期設定#

以降では、マンガ・アニメ・ゲームデータを可視化するための初期設定を行います。 なお、紙幅の都合のため、書籍版と一部構成が異なることにご注意ください。

Import#

必要なライブラリをImportします。

Hide code cell content
# warningsモジュールのインポート
import warnings

# データ解析や機械学習のライブラリ使用時の警告を非表示にする目的で警告を無視
# 本書の文脈では、可視化の学習に議論を集中させるために選択した
# ただし、学習以外の場面で、警告を無視する設定は推奨しない
warnings.filterwarnings("ignore")
Hide code cell content
# itertoolsモジュールのインポート
# 効率的なループを実行するためのイテレータビルディングブロックを提供
# これにより、データのコンビネーションや順列などを簡潔に表現できる
import itertools

# pathlibモジュールのインポート
# ファイルシステムのパスを扱う
from pathlib import Path

# typingモジュールからの型ヒント関連のインポート
# 関数やクラスの引数・返り値の型を注釈するためのツール
from typing import Any, Dict, List, Optional, Union

# numpy:数値計算ライブラリのインポート
# npという名前で参照可能
import numpy as np

# pandas:データ解析ライブラリのインポート
# pdという名前で参照可能
import pandas as pd

# plotly.expressのインポート
# インタラクティブなグラフ作成のライブラリ
# pxという名前で参照可能
import plotly.express as px

# plotly.graph_objectsからFigureクラスのインポート
# 型ヒントの利用を主目的とする
from plotly.graph_objects import Figure

なお、型ヒントについてはこちらを参照ください。

定数#

本Notebookで用いる定数を定義します。 なお、Pythonにおける定数の扱いについては、こちらを参照ください。

Hide code cell content
# マンガデータ保存ディレクトリのパス
DIR_CM = Path("../../data/cm/input")
# アニメデータ保存ディレクトリのパス
DIR_AN = Path("../../data/an/input")
# ゲームデータ保存ディレクトリのパス
DIR_GM = Path("../../data/gm/input")

# マンガデータの分析結果の出力先ディレクトリのパス
DIR_OUT_CM = DIR_CM.parent / "output" / Path.cwd().parts[-1] / "parallel"
# アニメデータの分析結果の出力先ディレクトリのパス
DIR_OUT_AN = DIR_AN.parent / "output" / Path.cwd().parts[-1] / "parallel"
# ゲームデータの分析結果の出力先ディレクトリのパス
DIR_OUT_GM = DIR_GM.parent / "output" / Path.cwd().parts[-1] / "parallel"
Hide code cell content
# 読み込み対象ファイル名の定義

# マンガ作品とマンガ作者の対応関係に関するファイル
FN_CC_CRT = "cm_cc_crt.csv"

# マンガ各話に関するファイル
FN_CE = "cm_ce.csv"

# アニメ作品と原作者の対応関係に関するファイル
FN_AC_ACT = "an_ac_act.csv"

# アニメ各話に関するファイル
FN_AE = "an_ae.csv"

# ゲームパッケージとプラットフォームの対応関係に関するファイル
FN_PKG_PF = "gm_pkg_pf.csv"
Hide code cell content
# 可視化に関する設定値の定義

# 「年代」の集計単位
UNIT_YEARS = 10
Hide code cell content
# plotlyの描画設定の定義

# plotlyのグラフ描画用レンダラーの定義
# Jupyter Notebook環境のグラフ表示に適切なものを選択
RENDERER = "plotly_mimetype+notebook"
Hide code cell content
# pandasのweekday関数で取得できる曜日の数値と実際の曜日名を対応させる辞書を定義
# 0:月曜日, 1:火曜日, ... , 6:日曜日
WEEKDAY2YOBI = {
    0: "月",
    1: "火",
    2: "水",
    3: "木",
    4: "金",
    5: "土",
    6: "日",
}
Hide code cell content
# 国内主要ゲームメーカーのプラットフォームとメーカー名の対応辞書
# キー: プラットフォーム名、値: メーカー名の略称
PF2MK = {
    "プレイステーション": "ソニー",
    "プレイステーション2": "ソニー",
    "プレイステーション・ポータブル": "ソニー",
    "プレイステーション3": "ソニー",
    "プレイステーションVita": "ソニー",
    "プレイステーション4": "ソニー",
    "ゲームアーカイブス": "ソニー",
    "SG-1000": "セガ",
    "SC-3000": "セガ",
    "SEGAマーク3": "セガ",
    "セガ・マスターシステム": "セガ",
    "メガドライブ": "セガ",
    "ゲームギア": "セガ",
    "セガサターン": "セガ",
    "ドリームキャスト": "セガ",
    "ファミリーコンピュータ": "任天堂",
    "ゲームボーイ": "任天堂",
    "スーパーファミコン": "任天堂",
    "NINTENDO64": "任天堂",
    "ゲームボーイアドバンス": "任天堂",
    "ニンテンドーゲームキューブ": "任天堂",
    "ニンテンドーDS": "任天堂",
    "ニンテンドー3DS": "任天堂",
    "Wii": "任天堂",
    "WiiU": "任天堂",
    "NintendoSwitch": "任天堂",
}
Hide code cell content
# 質的変数の描画用のカラースケールの定義

# Okabe and Ito (2008)基準のカラーパレット
# 色の識別性が高く、多様な色覚の人々にも見やすい色組み合わせ
# 参考URL: https://jfly.uni-koeln.de/color/#pallet
OKABE_ITO = [
    "#000000",  # 黒 (Black)
    "#E69F00",  # 橙 (Orange)
    "#56B4E9",  # 薄青 (Sky Blue)
    "#009E73",  # 青緑 (Bluish Green)
    "#F0E442",  # 黄色 (Yellow)
    "#0072B2",  # 青 (Blue)
    "#D55E00",  # 赤紫 (Vermilion)
    "#CC79A7",  # 紫 (Reddish Purple)
]

関数#

以下では、本Notebookで用いる関数を定義します。

Hide code cell content
def show_fig(fig: Figure) -> None:
    """
    所定のレンダラーを用いてplotlyの図を表示
    Jupyter Bookなどの環境での正確な表示を目的とする

    Parameters
    ----------
    fig : Figure
        表示対象のplotly図

    Returns
    -------
    None
    """

    # 図の周囲の余白を設定
    # t: 上余白
    # l: 左余白
    # r: 右余白
    # b: 下余白
    fig.update_layout(margin=dict(t=25, l=25, r=25, b=25))

    # 所定のレンダラーで図を表示
    fig.show(renderer=RENDERER)
Hide code cell content
def add_years_to_df(
    df: pd.DataFrame, unit_years: int = UNIT_YEARS, col_date: str = "date"
) -> pd.DataFrame:
    """
    データフレームにunit_years単位で区切った年数を示す新しい列を追加

    Parameters
    ----------
    df : pd.DataFrame
        入力データフレーム
    unit_years : int, optional
        年数を区切る単位、デフォルトはUNIT_YEARS
    col_date : str, optional
        日付を含むカラム名、デフォルトは "date"

    Returns
    -------
    pd.DataFrame
        新しい列が追加されたデータフレーム
    """

    # 入力データフレームをコピー
    df_new = df.copy()

    # unit_years単位で年数を区切り、新しい列として追加
    df_new["years"] = (
        pd.to_datetime(df_new[col_date]).dt.year // unit_years * unit_years
    )

    # 'years'列のデータ型を文字列に変更
    df_new["years"] = df_new["years"].astype(str)

    return df_new
Hide code cell content
def add_id_to_df(df: pd.DataFrame, column_name: str) -> pd.DataFrame:
    """
    指定されたカラムのユニークな値に対してIDを付与する

    Parameters
    ----------
    df : pandas.DataFrame
        IDを付与する対象のDataFrame
    column_name : str
        IDを付与するカラムの名前

    Returns
    -------
    pandas.DataFrame
        更新されたDataFrame。元のDataFrameには影響を与えない
    """

    # 元のDataFrameをコピーして、新しいDataFrameを作成
    df_new = df.copy()

    # 指定されたカラムのユニークな値を取得して、それぞれにIDを割り当てる
    value2id = {x: i for i, x in enumerate(sorted(df[column_name].unique()))}

    # 新しいカラムを作成して、それぞれの値に対応するIDを割り当てる
    df_new[f"{column_name}ID"] = df_new[column_name].map(value2id)

    # 更新されたDataFrameを返す
    return df_new
Hide code cell content
def format_cols(df: pd.DataFrame, cols_rename: Dict[str, str]) -> pd.DataFrame:
    """
    指定されたカラムのみをデータフレームから抽出し、カラム名をリネームする関数

    Parameters
    ----------
    df : pd.DataFrame
        入力データフレーム
    cols_rename : Dict[str, str]
        リネームしたいカラム名のマッピング(元のカラム名: 新しいカラム名)

    Returns
    -------
    pd.DataFrame
        カラムが抽出・リネームされたデータフレーム
    """

    # 指定されたカラムのみを抽出し、リネーム
    df = df[cols_rename.keys()].rename(columns=cols_rename)

    return df
Hide code cell content
def save_df_to_csv(df: pd.DataFrame, dir_save: Path, fn_save: str) -> None:
    """
    DataFrameをCSVファイルとして指定されたディレクトリに保存する関数

    Parameters
    ----------
    df : pd.DataFrame
        保存対象となるDataFrame
    dir_save : Path
        出力先ディレクトリのパス
    fn_save : str
        保存するCSVファイルの名前(拡張子は含めない)
    """
    # 出力先ディレクトリが存在しない場合は作成
    dir_save.mkdir(parents=True, exist_ok=True)

    # 出力先のパスを作成
    p_save = dir_save / f"{fn_save}.csv"

    # DataFrameをCSVファイルとして保存する
    df.to_csv(p_save, index=False, encoding="utf-8-sig")

    # 保存完了のメッセージを表示する
    print(f"DataFrame is saved as '{p_save}'.")

可視化例#

マンガデータ#

マンガ作品数を例に、可視化手法を説明します。

Hide code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_cc_crt = pd.read_csv(DIR_CM / FN_CC_CRT)
Hide code cell content
# ccidごとの最初の登場年代を格納するDataFrameを作成
# まず、ccidごとにgroupbyし、最も古いfirst_dateを抽出
df_cc_fyears = (
    df_cc_crt.groupby("ccid")["first_date"].min().reset_index(name="first_date")
)
# first_dateをもとにUNIT_YEARS単位でfirst_years列を計算
df_cc_fyears["first_years"] = (
    pd.to_datetime(df_cc_fyears["first_date"]).dt.year // UNIT_YEARS * UNIT_YEARS
)

# ccidごとのユニークなcrt数を集計し、単著か共著かを判断
# このとき、可視化で使うためmcnameも列として残しておくことに注意
df_cc_ncrt = (
    df_cc_crt.groupby(["ccid", "ccname", "mcname"])["crtid"]
    .nunique()
    .reset_index(name="n_crt")
)
# n_crt==1の場合は単独制作、それ以外は共同制作とする
df_cc_ncrt["n_crt"] = df_cc_ncrt["n_crt"].apply(
    lambda x: "単独制作" if x == 1 else "共同制作"
)

# df_cc_yearsとdf_cc_crtを結合
df_cm = pd.merge(df_cc_ncrt, df_cc_fyears, on="ccid")

ここで、可視化対象に重複がないよう、ccidがデータ中でユニークであるかどうか確認します。

Hide code cell content
# ccidが重複した行数が0であることを確認
assert df_cm["ccid"].duplicated().sum() == 0

AssertionErrorが出なかったため、このまま分析を進めます。

Hide code cell content
# 可視化時に見やすい順序に並ぶよう並び替え
df_cm = df_cm.sort_values(["mcname", "first_years", "n_crt"], ignore_index=True)

# 可視化のために列名を変更
cols_cm = {
    "ccname": "マンガ作品名",
    "mcname": "マンガ雑誌名",
    "first_years": "初出年代",
    "n_crt": "制作形態",
}
df_cm = format_cols(df_cm, cols_cm)

# 可視化用にint型のmcidを追加
df_cm = add_id_to_df(df_cm, "マンガ雑誌名")
Hide code cell content
# 可視化対象のDataFrmeを確認
df_cm.head()
マンガ作品名 マンガ雑誌名 初出年代 制作形態 マンガ雑誌名ID
0 QBジョーのタッチダウン 週刊少年サンデー 1970 共同制作 0
1 青い目のバンチョウ 週刊少年サンデー 1970 共同制作 0
2 明日にむかって走れ! 週刊少年サンデー 1970 共同制作 0
3 当たり屋 週刊少年サンデー 1970 共同制作 0
4 あばれフブキ 週刊少年サンデー 1970 共同制作 0
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_cm, DIR_OUT_CM, "cm")
DataFrame is saved as '../../data/cm/output/07/parallel/cm.csv'.
Hide code cell source
# Plotly Expressを使って並行カテゴリ図を作成
# 'マンガ雑誌名', '初出年代', '制作形態' のカテゴリを基にして、'マンガ雑誌名ID'の値で色分け
# color_continuous_scaleで質的変数のカラーマップを設定するとき、水準数と同じサイズのリストを渡すと良い
fig = px.parallel_categories(
    df_cm,
    dimensions=["マンガ雑誌名", "制作形態", "初出年代"],
    color="マンガ雑誌名ID",
    color_continuous_scale=OKABE_ITO[:4],
)

# カラーバーの表示をオフに設定
fig.update_coloraxes(showscale=False)

# 作成した図を表示
show_fig(fig)

上図は、マンガ雑誌・初出年代・作者数別のマンガ作品の内訳を表現したパラレルセットグラフです。 一番左の列は各マンガ雑誌別の内訳を、中央の列はマンガ作者数(単著、あるいは共著)別の内訳を、そして右の列は年代別の内訳を表現しています。 色はマンガ雑誌と対応しているため、それぞれの観点におけるマンガ雑誌の割合を確認することができます。

全体を通して、ほぼ均等に分布していることがわかります。

パラレルセットグラフを見やすくする工夫として、以下二点があると言われています[Wilke et al., 2022]

  • (我々は左から読むことに慣れているため)一番左の列を基準に配色すること

  • 列間の要素の交差が可能な限り少なくなるような順序で、列を配置すること

特に一点目は重要です。試しに、中央の列(制作形態)を基準に配色してみましょう。

Hide code cell content
# 制作形態列に応じてint型のidを付与(年代列はstr型)
df_cm = add_id_to_df(df_cm, "制作形態")
Hide code cell source
# Plotly Expressを使って並行カテゴリ図を作成
# 'マンガ雑誌名', '制作形態, '初出年代' のカテゴリを基にして、'制作形態ID'の値で色分け
# color_continuous_scaleで質的変数のカラーマップを設定するとき、水準数と同じサイズのリストを渡すと良い
fig = px.parallel_categories(
    df_cm,
    dimensions=["マンガ雑誌名", "制作形態", "初出年代"],
    color="制作形態ID",
    color_continuous_scale=OKABE_ITO[:2],
)

# カラーバーの表示をオフに設定
fig.update_coloraxes(showscale=False)

# 作成した図を表示
show_fig(fig)

中央の列にばかり注目が集まり、左右の列をどのような順番で見てよいかわかりません。

制作形態について語りたい場合は、一番左の列に配置した上で、配色の基準とすると良いでしょう。

Hide code cell source
# Plotly Expressを使って並行カテゴリ図を作成
# '制作形態', 'マンガ雑誌名', '初出年代' のカテゴリを基にして、'制作形態ID'の値で色分け
# color_continuous_scaleで質的変数のカラーマップを設定するとき、水準数と同じサイズのリストを渡すと良い
fig = px.parallel_categories(
    df_cm,
    dimensions=["制作形態", "マンガ雑誌名", "初出年代"],
    color="制作形態ID",
    color_continuous_scale=OKABE_ITO[:2],
)

# カラーバーの表示をオフに設定
fig.update_coloraxes(showscale=False)

# 作成した図を表示
show_fig(fig)

左から右に見る というメッセージが明確になったのではないでしょうか?

若干ではありますが、他の年代より1970年代における共著の割合が大きいことがわかります。

アニメデータ#

アニメ作品数を例に、可視化手法を説明します。

Hide code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_ae = pd.read_csv(DIR_AN / FN_AE)
Hide code cell content
# datetime型に変換
df_ae["date"] = pd.to_datetime(df_ae["date"])
Hide code cell content
# acidの最初と最後のdateを集計
df_an = (
    df_ae.groupby("acid")
    .agg(first_date=("date", "min"), last_date=("date", "max"))
    .reset_index()
)

# 最初に放送された年代をfirst_years列として追加
df_an["first_years"] = df_an["first_date"].dt.year // UNIT_YEARS * UNIT_YEARS

# 最初に放送された曜日をfirst_weekday列として追加
df_an["first_weekday"] = df_an["first_date"].dt.weekday
# その曜日をfirst_yobi列として追加
df_an["first_yobi"] = df_an["first_weekday"].map(WEEKDAY2YOBI)

# 最初と最後の放送日の間の間隔を計算
df_an["duration"] = (df_an["last_date"] - df_an["first_date"]).dt.days

# durationが3ヶ月(90日)以上のものを複数クール作品として判定
df_an["n_cours"] = df_an["duration"].apply(
    lambda x: "2クール以上" if x > 90 else "1クール以下"
)

ここで、可視化対象に重複がないよう、acidがデータ中でユニークであるかどうか確認します。

Hide code cell content
# acidが重複する行数が0であることを確認
assert df_an["acid"].duplicated().sum() == 0

また、念のため年代別のサンプルサイズを確認しておきます。

Hide code cell content
# 'first_years' でグループ化し、各グループ内のユニークな 'acid' の数をカウント
df_an.groupby("first_years")["acid"].nunique().reset_index()
first_years acid
0 1960 2
1 1970 4
2 1990 454
3 2000 1546
4 2010 1631

1980年代までのサンプルサイズが極端に少ないため、1990年代以降を可視化対象としましょう。

Hide code cell content
# df_anから1990年代以降のデータのみをフィルタリング
df_an = df_an[df_an["first_years"].astype(int) >= 1990].reset_index(drop=True)
Hide code cell content
# 可視化用に各列の要素でソート
df_an = df_an.sort_values(
    ["first_years", "first_weekday", "duration"], ignore_index=True
)

# 可視化用に列名を変更
cols_an = {
    "acid": "アニメ作品名ID", 
    "first_years": "初出年代",
    "first_yobi": "初出曜日",
    "n_cours": "クール数",
    "first_weekday": "初出曜日ID",
}
df_an = format_cols(df_an, cols_an)

# 可視化用にfirst_yearsを基準にint型のidを付与
df_an = add_id_to_df(df_an, "初出年代")
Hide code cell content
# 可視化対象のDataFrameを確認
df_an.head()
アニメ作品名ID 初出年代 初出曜日 クール数 初出曜日ID 初出年代ID
0 C9867 1990 1クール以下 0 0
1 C9418 1990 1クール以下 0 0
2 C9869 1990 1クール以下 0 0
3 C9110 1990 1クール以下 0 0
4 C9066 1990 1クール以下 0 0
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_an, DIR_OUT_AN, "an")
DataFrame is saved as '../../data/an/output/07/parallel/an.csv'.
Hide code cell source
# Plotly Expressを使って並行カテゴリ図を作成
# '初出年代', 'クール数', '初出曜日' のカテゴリを基にして、'初出年代ID' の値で色分け
# 色のスケールには Portland スタイルを使用
fig = px.parallel_categories(
    df_an,
    dimensions=["初出年代", "クール数", "初出曜日"],
    color="初出年代ID",
    color_continuous_scale=px.colors.diverging.Portland,
)

# カラーバーの表示をオフに設定
fig.update_coloraxes(showscale=False)

# 作成した図を表示
show_fig(fig)

上図は、初出年代別・クール数別・初出曜日別のアニメ作品の内訳を示したパラレルセットグラフです。 左側の列が初出年代の内訳を、中央の列がクール数(1クール以下2クール以上)の内訳を、右側の列が初出曜日の内訳を表現しています。

まず初出年代という観点では、1990年代より、20002010年代のアニメ作品数が多いことがわかります。 次に放送クール数という観点では、1クール以下で終わってしまう作品のほうが、2クール以上続く作品より比較的多いことがわかります。 また、2010年代の方が、2000年代より1クール以下の作品の割合が大きいこともわかります。 最後に放送曜日という観点では、目視では各曜日の内訳の違いはほとんどわかりません。

次に、放送曜日を起点に分析してみましょう。

Hide code cell source
# Plotly Expressを使って並行カテゴリ図を作成
# '初出曜日', 'クール数', '初出年代' のカテゴリを基にして、'初出曜日ID' の値で色分け
# 色のスケールには OKABE_ITO の最初の7色を使用
fig = px.parallel_categories(
    df_an,
    dimensions=["初出曜日", "クール数", "初出年代"],
    color="初出曜日ID",
    color_continuous_scale=OKABE_ITO[:7],
)

# カラーバーの表示をオフに設定
fig.update_coloraxes(showscale=False)

# 作成した図を表示
show_fig(fig)

上図は、初出曜日を一番左側に移動したパラレルセットグラフです。 曜や曜に放送されたアニメ作品の方が、2クール以上の放送となる割合が多いということがわかります。

パラレルセットグラフでは、列として登場しない変数を基準にした配色を指定することができます。 例えば、放送曜日が平日か土日かで色をわけることを考えてみましょう。

Hide code cell content
# 土日か否かをweekdayを基準に判断
df_an["土日"] = (df_an["初出曜日ID"] >= 5).astype(int)
Hide code cell source
# Plotly Expressを使って並行カテゴリ図を作成
# '初出曜日', 'クール数', '初出年代' のカテゴリを基にして、'土日' の値で色分け
# 色のスケールには OKABE_ITO の最初の2色を使用
fig = px.parallel_categories(
    df_an,
    dimensions=["初出曜日", "クール数", "初出年代"],
    color="土日",
    color_continuous_scale=OKABE_ITO[:2],
)

# カラーバーの表示をオフに設定
fig.update_coloraxes(showscale=False)

# 作成した図を表示
show_fig(fig)

このように表現することで、放送曜日が平日か否かを強調できるようになりました。 ただ、このように新たな変数を用いて配色する場合においても、一番左側の列に基づいたものにする(一番左側の列中で色が混在しない)よう注意しましょう。

ゲームデータ#

ゲームパッケージ数を例に、可視化手法を説明します。

Hide code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_pkg_pf = pd.read_csv(DIR_GM / FN_PKG_PF)

パラレルセットグラフの可視化対象となる変数(今回はゲームパッケージ)は、データ中でユニークである必要があります。

Hide code cell content
# 重複のあるゲームパッケージ一覧を抽出
pkgids = df_pkg_pf[df_pkg_pf["pkgid"].duplicated()]["pkgid"].unique().tolist()
df_pkg_pf[df_pkg_pf["pkgid"].isin(pkgids)]
pkgid pfid pkgname publisher date price pfname
653 M719684 PF00005 AIが止まらない! 新装版 0⇒9 新装版 講談社 2000-11-02 3800.0 MicrosoftWindows
654 M719684 PF00022 AIが止まらない! 新装版 0⇒9 新装版 講談社 2000-11-02 3800.0 macOS
691 M719729 PF00005 王宮夜想曲 月下氷人 2005-09-22 7140.0 MicrosoftWindows
692 M719729 PF00022 王宮夜想曲 月下氷人 2005-09-22 7140.0 macOS
721 M719759 PF00005 ボクの彼氏はジュリエット サンダルダッシュ,イーアンツ有限会社 2002-10-11 6800.0 MicrosoftWindows
... ... ... ... ... ... ... ...
7724 M727844 PF00014 チャンピオン ボクシング セガ・エンタープライゼス 1984-12-01 4300.0 SG-1000
9021 M735214 PF00022 ときめきメモリアル2タイピング アクティマインド; コナミ 2003-05-22 5200.0 macOS
9022 M735214 PF00005 ときめきメモリアル2タイピング アクティマインド; コナミ 2003-05-22 5200.0 MicrosoftWindows
9023 M735215 PF00022 ときめきメモリアルタイピング アクティマインド; コナミ 2002-04-25 4900.0 macOS
9024 M735215 PF00005 ときめきメモリアルタイピング アクティマインド; コナミ 2002-04-25 4900.0 MicrosoftWindows

73 rows × 7 columns

df_pkg_pfにおいて、少数ではありますがゲームパッケージに重複があることがわかります。 これはゲームデータの基礎分析でも触れた通り、複数のゲームプラットフォームに紐づくゲームパッケージが存在するためです。 重複のあるゲームパッケージを可視化対象から除外しておきましょう。

Hide code cell content
# 複数のゲームプラットフォームに紐づくゲームパッケージを可視化対象から除外
df_gm = df_pkg_pf[~df_pkg_pf["pkgid"].isin(pkgids)].reset_index(drop=True)

# 念のためassert文で重複がないことを再確認
assert df_gm["pkgid"].duplicated().sum() == 0
Hide code cell content
# 年代情報をdf_gmに追加
df_gm = add_years_to_df(df_gm, unit_years=10)

# PF2MK(プラットフォームとメーカーの対応辞書)でカバーしているプラットフォームのみ抽出
df_gm = df_gm[df_gm["pfname"].isin(PF2MK)].reset_index(drop=True)
# maker列にメーカー情報を追加
df_gm["maker"] = df_gm["pfname"].map(PF2MK)

# 曜日を表すint値をdateから算出し、weekday列として追加
df_gm["weekday"] = pd.to_datetime(df_gm["date"]).dt.weekday
# weekdayを日本の曜日情報に変換し、yobi列として追加
df_gm["yobi"] = df_gm["weekday"].map(WEEKDAY2YOBI)

念のため、年代ごとのサンプルサイズを確認しておきましょう。

Hide code cell content
# yearsごとのpkgid数を集計して表示
df_gm.groupby("years")["pkgid"].nunique().reset_index()
years pkgid
0 1980 324
1 1990 5871
2 2000 11789
3 2010 13661

1980年代まで極端にサンプルサイズが少ないことがわかります。1990年代以降を可視化対象としましょう。

Hide code cell content
# 1990年代以降を可視化対象として抽出
# なお、years列はstr型のため、astype(int)でint型にキャストしてから不等式を適用
df_gm = df_gm[df_gm["years"].astype(int) >= 1990].reset_index(drop=True)
Hide code cell content
# 可視化した際に見やすいよう、事前にソートしておく
df_gm = df_gm.sort_values(["maker", "weekday", "date"], ignore_index=True)

# 可視化のために列名を変更
cols_gm = {
    "pkgid": "パッケージID",
    "years": "発売年代",
    "maker": "メーカー名",
    "pfname": "プラットフォーム名",
    "yobi": "発売曜日",
    "weekday": "発売曜日ID",
}
df_gm = format_cols(df_gm, cols_gm)

# 可視化用に年代列に基づいたint型のid列を追加
df_gm = add_id_to_df(df_gm, "発売年代")
Hide code cell content
# 可視化対象のDataFrameを確認
df_gm.head()
パッケージID 発売年代 メーカー名 プラットフォーム名 発売曜日 発売曜日ID 発売年代ID
0 M719593 1990 セガ セガサターン 0 0
1 M760877 1990 セガ セガサターン 0 0
2 M753265 2000 セガ ドリームキャスト 0 1
3 M719176 2000 セガ ドリームキャスト 0 1
4 M727306 1990 セガ メガドライブ 1 0
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_gm, DIR_OUT_GM, "gm")
DataFrame is saved as '../../data/gm/output/07/parallel/gm.csv'.
Hide code cell source
# Plotly Expressを使ってパラレルセットグラフを作成
# '発売年代', 'メーカー名', '発売曜日' のカテゴリを基にして、'発売年代ID' の値で色分け
# 色のスケールには Portland スタイルを使用
fig = px.parallel_categories(
    df_gm,
    dimensions=["発売年代", "メーカー名", "発売曜日"],
    color="発売年代ID",
    color_continuous_scale=px.colors.diverging.Portland,
)

# カラーバーの表示をオフに設定
fig.update_coloraxes(showscale=False)

# 作成した図を表示
show_fig(fig)

上図は、年代別・メーカー別・発売曜日別のゲームパッケージ数の内訳を表現したパラレルセットグラフです。

一番左の列は年代別の内訳、中央の列はメーカー別の内訳、一番右の列は発売曜日別の内訳を表します。 年代という観点ですと、1990年代からゲームパッケージ数が増え続けていることがわかります。 メーカーという観点ですと、ソニー[1]が最も多く、任天堂マイクロソフトセガが続きます。 発売曜日という観点ですと、曜が最も多く、曜・曜が続くことは、これまでの他の可視化手法でも触れてきたとおりです。

年代を基準とした配色を行っているため、年代と他の観点を組み合わせた内訳を表現することができます。 例えば、中央の列を見てみましょう。 セガ1990年代の割合が大きい一方で、ソニー任天堂は、全ての年代でバランスよくゲームパッケージを発売しています。

次に、一番左の列を見ると、発売曜日と年代の組み合わせの内訳がわかります。 2000年代以降の割合が多い一方で、曜は1990年代の割合が多いです。

パラレルセットグラフでは、四つ以上の質的変数を対象にすることも可能です。 例えば、上図にゲームプラットフォームの情報を追加してみましょう。

Hide code cell source
# Plotly Expressを使ってパラレルセットグラフを作成
# '発売年代', 'メーカー名', 'プラットフォーム名', '発売曜日' のカテゴリを基にして、'発売年代ID' の値で色分け
# 色のスケールには Portland スタイルを使用
# プラットフォーム数が多いため、heightで高さを調整
fig = px.parallel_categories(
    df_gm,
    dimensions=["発売年代", "メーカー名", "プラットフォーム名", "発売曜日"],
    color="発売年代ID",
    color_continuous_scale=px.colors.diverging.Portland,
    height=600,
)

# カラーバーの表示をオフに設定
fig.update_coloraxes(showscale=False)

# 作成した図を表示
show_fig(fig)

上図は、年代別・メーカー別・プラットフォーム別・発売曜日別のゲームパッケージ数の内訳を表現したパラレルセットグラフです。 少し見づらくなってしまいますが、各メーカーの年代別のゲームパッケージ数が、それぞれどのプラットフォームに支えられていたかわかりやすくなりました。 例えば、ソニー1990年代のゲームパッケージは、プレイステーション向けであることがわかります。

一方で、メーカーやプラットフォームと発売曜日の関係は非常に複雑になってしまいました。 一旦年代列を除外し、メーカー・プラットフォーム・発売曜日に絞って可視化してみましょう。

Hide code cell content
# 可視化用にメーカー名に対するint型のidを付与
df_gm = add_id_to_df(df_gm, "メーカー名")
Hide code cell source
# Plotly Expressを使ってパラレルセットグラフを作成
# 'メーカー名', 'プラットフォーム名', '発売曜日' のカテゴリを基にして、'メーカー名ID' の値で色分け
# 色のスケールには OKABE_ITO の最初の3色を使用
# プラットフォーム数が多いため、heightで高さを調整
fig = px.parallel_categories(
    df_gm,
    dimensions=["メーカー名", "プラットフォーム名", "発売曜日"],
    color="メーカー名ID",
    color_continuous_scale=OKABE_ITO[:3],
    height=600,
)

# カラーバーの表示をオフに設定
fig.update_coloraxes(showscale=False)

# 作成した図を表示
show_fig(fig)

上図は、メーカー・プラットフォーム・発売曜日別のゲームパッケージ数の内訳を示したパラレルセットグラフです。 メーカー名を基準に色分けしています。

一番右側の発売曜日の列を見ると、各曜日におけるゲームメーカーの内訳を確認できます。 最も発売パッケージ数の多い曜は、そのほとんどをソニーが占めていたことがわかります。 曜に関しては、任天堂が最も多く、そのほとんどをゲームボーイスーパーファミコン等が占めていたことがわかります。 曜に関しては、ソニー任天堂が半分程度であり、ソニーからは主にゲームアーカイブスが、任天堂からは主にニンテンドーDSニンテンドー3DSWiiUが占めていたようです。

では、発売曜日を基準に再度パラレルセットグラフを作成し、別の観点から分析してみましょう。

Hide code cell source
# Plotly Expressを使ってパラレルセットグラフを作成
# '発売曜日', 'プラットフォーム名', 'メーカー名' のカテゴリを基にして、'発売曜日ID' の値で色分け
# 色のスケールには OKABE_ITO の最初の7色を使用
# プラットフォーム数が多いため、heightで高さを調整
fig = px.parallel_categories(
    df_gm,
    dimensions=["発売曜日", "プラットフォーム名", "メーカー名"],
    color="発売曜日ID",
    color_continuous_scale=OKABE_ITO[:7],
    height=600,
)

# カラーバーの表示をオフに設定
fig.update_coloraxes(showscale=False)

# 作成した図を表示
show_fig(fig)

上図は、発売曜日を基準としたパラレルセットグラフです。 各メーカーやプラットフォームのパッケージが、どの曜日に発売されていたかわかりやすくなりました。

基本的にソニーのゲームプラットフォーム向けのゲームパッケージは曜に発売されていますが、プレイステーション(一部曜)とゲームアーカイブス(ほぼ全て曜)は例外です。

任天堂に関しては、ゲームボーイスーパーファミコンゲームボーイアドバンス用のゲームパッケージはほぼ曜に発売されており、その他のプラットフォームは曜と曜に発売されていました。 ただし、Wiiに関しては、曜にゲームパッケージを発売することが多かったようです。 これは他メーカーのどのプラットフォームと比較しても例外的です。

セガに関しては、メガドライブゲームギアはほぼ全て曜、セガサターンから徐々に曜から曜発売に移行し、ドリームキャストではほぼ全てのゲームパッケージを曜に発売していました。